<template>
  <div class="simple-upload-container" style="height:300px;overflow-y:scroll">
    <div class="total-progress">
      <div class="btns">
        <el-button-group>
          <el-button type="primary" :disabled="changeDisabled">
            <i class="el-icon-upload2 el-icon--left" size="mini"></i>选择文件
            <input v-if="!changeDisabled" id="fileInput" type="file" :multiple="multiple" class="select-file-input" :accept="accept" οnclick="f.outerHTML=f.outerHTML" @change="handleFileChange" />
          </el-button>
          <el-button type="primary" :disabled="uploadDisabled" @click="handleUpload"><i class="el-icon-upload el-icon--left" size="mini"></i>上传/续传</el-button>
          <el-button type="primary" :disabled="pauseDisabled" @click="handlePause"><i class="el-icon-video-pause el-icon--left" size="mini"></i>暂停</el-button>
          <el-button type="primary" :disabled="resumeDisabled" @click="handleResume"><i class="el-icon-video-play el-icon--left" size="mini"></i>恢复</el-button>
          <el-button type="primary" :disabled="clearDisabled" @click="handleClearFiles"><i class="el-icon-remove-outline el-icon--left" size="mini"></i>清空</el-button>
        </el-button-group>
        <slot name="header"></slot>
      </div>
    </div>
    <div class="file-list">
      <div v-for="(item, index) in uploadFiles" :key="index" class="text item">
        <div class="progress-box">
          <div class="list-item">
            <el-row>
              <el-col :span="8">
                <div class="item-name">
                  <span>{{ index + 1 }}：{{ item.name }}</span>
                </div>
              </el-col>
              <el-col :span="4">
                <div class="item-size" style="text-align:right">大小：{{ item.size | transformByte }}</div>
              </el-col>
              <el-col :span="8">
                <div v-if="item.hashProgress !== 100" class="item-progress">
                  <span>{{ status === 'wait' ? '准备读取文件' : '正在读取文件' }}：</span>
                  <el-progress :percentage="item.hashProgress" />
                </div>
                <div v-else class="item-progress"><span>文件上传进度：</span><el-progress :percentage="item.uploadProgress" /></div>
              </el-col>
              <el-col :span="4">
                <div class="item-status" >
                  <i :class="fileStatuIcon(item.status)"></i>
                  {{ item.status | fileStatus }}
                </div>
              </el-col>
            </el-row>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import axios from '@tapui/utils/request'
import { CancelToken } from 'axios'
import { putAction, getAction, postAction, deleteAction } from '@tapui/utils/http'
var chunkSize = 3 * 1024 * 1024 // 切片大小
var fileIndex = 0 // 当前正在被遍历的文件下标
import SparkMD5 from 'spark-md5'
// 所有文件状态
const Status = {
  wait: 'wait',
  pause: 'pause',
  uploading: 'uploading',
  hash: 'hash',
  error: 'error',
  done: 'done'
}
// 单个文件的状态
const fileStatus = {
  wait: 'wait',
  uploading: 'uploading',
  success: 'success',
  error: 'error',
  secondPass: 'secondPass',
  pause: 'pause',
  resume: 'resume'
}
// 单个文件的状态 对应描述
const fileStatusStr = {
  wait: '待上传',
  uploading: '上传中',
  success: '成功',
  error: '失败',
  secondPass: '已秒传',
  pause: '暂停',
  resume: '恢复'
}
export default {
  name: 'SimpleUploaderContainer',
  filters: {
    transformByte(size) {
      if (!size) {
        return '0B'
      }
      var num = 1024.0 // byte
      if (size < num) {
        return size + 'B'
      }
      if (size < Math.pow(num, 2)) {
        return (size / num).toFixed(2) + 'K'
      } // kb
      if (size < Math.pow(num, 3)) {
        return (size / Math.pow(num, 2)).toFixed(2) + 'M'
      } // M
      if (size < Math.pow(num, 4)) {
        return (size / Math.pow(num, 3)).toFixed(2) + 'G'
      } // G
      return (size / Math.pow(num, 4)).toFixed(2) + 'T' // T
    },
    // 单个文件状态格式化
    fileStatus(status) {
      return fileStatusStr[fileStatus[status]]
    }
  },
  props: {
    beforeUpload: {
      type: Function,
      default: null
    },
    accept: {
      type: String,
      default: ''
    },
    // 上传文件时携带的参数
    uploadArguments: {
      type: Object,
      default: () => {
        return {}
      }
    },
    // 文件个数
    limit: {
      type: Number,
      default: 0
    },
    // 文件超出个数限制时的钩子
    onExceed: {
      type: Function,
      default: () => {}
    },
    multiple: {
      type: Boolean,
      default: true
    },
    // axios baseUrl
    baseUrl: {
      type: String,
      default: ''
    },
    // 切片大小
    chunkSize: {
      type: Number,
      default: 10 * 1024 * 1024
    },
    // 上传并发数
    threads: {
      type: Number,
      default: 1
    },
    // 错误重试次数
    chunkRetry: {
      type: Number,
      default: 3
    },
    sourceType: {
      type: String,
      default: ''
    },
    sourceId: {
      type: [Number, String],
      default: ''
    }
  },
  data: () => ({
    uploadFiles: [],
    worker: null,
    cancels: [], // 存储要取消的请求
    tempThreads: 3,
    // 默认状态
    status: Status.wait,
    url: {
      uploadFileBlock: '/meet/fileManage/uploadFileBlock',
      mergeFileBlocks: '/meet/fileManage/mergeFileBlocks',
      queryAlreadyUploadBlocksNum: '/meet/fileManage/queryUploadBlocks'
    }
  }),
  computed: {
    changeDisabled() {
      return ![Status.wait, Status.done].includes(this.status)
    },
    uploadDisabled() {
      return !this.uploadFiles.length || [Status.pause, Status.done, Status.uploading, Status.hash].includes(this.status)
    },
    pauseDisabled() {
      return [Status.hash, Status.wait, Status.done, Status.pause].includes(this.status)
    },
    resumeDisabled() {
      return ![Status.pause].includes(this.status)
    },
    clearDisabled() {
      return !this.uploadFiles.length
    },
    fileStatuIcon(status) {
      return function(status) {
        var className = ''
        switch (status) {
          case 'uploading':
            className = 'el-icon-loading'
            break
          case 'success':
          case 'secondPass':
            className = 'el-icon-circle-check'
            break
          case 'error':
            className = 'el-icon-circle-close'
            break
        }
        return className + ' el-icon--left'
      }
    }
  },
  destroyed() {
    this.clearFiles()
  },
  created() {
    this.tempThreads = this.threads
  },
  methods: {
    // 选择文件
    handleFileChange(e) {
      const files = e.target.files
      if (!files) return
      // 重置文件下标
      fileIndex = 0
      // 判断文件选择的个数
      if (this.limit && files.length > this.limit) {
        this.onExceed && this.onExceed(files)
        return
      }
      this.status = Status.wait
      const postFiles = Array.prototype.slice.call(files)
      // 添加到文件上传队列
      postFiles.forEach(item => {
        this.handleStart(item)
      })
      document.getElementById('fileInput').value = ''
    },
    // 初始化选择的文件
    handleStart(rawFile) {
      // 初始化部分自定义属性
      rawFile.status = fileStatus.wait
      rawFile.chunkList = []
      rawFile.uploadProgress = 0
      rawFile.fakeUploadProgress = 0 // 假进度条，处理恢复上传后，进度条后移的问题
      rawFile.hashProgress = 0
      if (this.beforeUpload) {
        const before = this.beforeUpload(rawFile)
        if (before) {
          this.uploadFiles.push(rawFile)
        }
      } else {
        this.uploadFiles.push(rawFile)
      }
    },
    // 文件上传
    async handleUpload() {
      if (!this.uploadFiles) return
      this.status = Status.uploading
      const filesArr = this.uploadFiles
      for (let i = 0; i < filesArr.length; i++) {
        fileIndex = i
        if (['secondPass', 'success', 'error'].includes(filesArr[i].status)) {
          continue
        }
        // 若不是恢复，再进行hash计算
        if (filesArr[i].status !== 'resume') {
          this.status = Status.hash
          // hash校验，是否为秒传
          filesArr[i].hash = await this.computeMD5(filesArr[i])
          // 若清空或者状态为等待，则跳出循环
          if (this.status === Status.wait) {
            break
          }
        }
        this.status = Status.uploading
        //  查询已经上传的块
        const verifyRes = await this.verifyUpload(filesArr[i].name, filesArr[i].hash)
        // 已上传的块号
        let alreadyUploadBlocksnum = []
        let blockSize = chunkSize
        if (verifyRes[filesArr[i].hash]) {
          let blockMap = verifyRes[filesArr[i].hash]
          alreadyUploadBlocksnum = blockMap['uploadblocklist']
          blockSize = blockMap['blocksize']
        }
        // 切片
        const fileChunkList = this.createFileChunk(filesArr[i], blockSize)
        /* if (alreadyUploadBlocksnum && alreadyUploadBlocksnum.length === fileChunkList.length) {
          // 秒传成功
          filesArr[i].status = fileStatus.secondPass
          filesArr[i].uploadProgress = 100
          await this.mergeRequest(filesArr[i])
          // 判断是否所有都成功上传
          this.isAllStatus()
        } else {*/
        filesArr[i].status = fileStatus.uploading
        filesArr[i].fileHash = filesArr[i].hash // 文件的hash，合并时使用
        filesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({
          fileHash: filesArr[i].hash,
          fileName: filesArr[i].name,
          index,
          md5: filesArr[i].hash,
          chunk: file,
          size: file.size,
          blocksize: blockSize,
          filesize: filesArr[i].size,
          uploaded: alreadyUploadBlocksnum && alreadyUploadBlocksnum.includes(index + 1), // 标识：是否已完成上传
          progress: alreadyUploadBlocksnum && alreadyUploadBlocksnum.includes(index + 1) ? 100 : 0,
          status: alreadyUploadBlocksnum && alreadyUploadBlocksnum.includes(index + 1) ? 'success' : 'wait' // 上传状态，用作进度状态显示
        }))
        this.$set(filesArr, i, filesArr[i])
        // 文件上传
        await this.uploadChunks(filesArr[i])
      }
    },
    // 将文件切片传输给服务端
    async uploadChunks(data) {
      var chunkData = data.chunkList
      return new Promise(async(resolve, reject) => {
        const requestDataList = chunkData
          .filter(({ uploaded }) => !uploaded)
          .map(({ fileHash, chunk, fileName, blocksize, filesize, index }) => {
            const formData = new FormData()
            formData.append('file', chunk)
            formData.append('blocknum', index + 1)
            formData.append('blocksize', blocksize)
            formData.append('filename', fileName)
            formData.append('filemd5', fileHash)
            formData.append('identifier', fileHash)
            formData.append('totalblockcount', chunkData.length)
            formData.append('filesize', filesize)
            formData.append('blocksize', chunkSize)
            formData.append('sourceid', this.sourceId)
            formData.append('sourcetype', this.sourceType)
            formData.append('type', this.getFileExt(fileName))
            return { formData, index, fileName }
          })
        try {
          const ret = await this.sendRequest(requestDataList, chunkData)
        } catch (error) {
          // 上传有被reject的
          this.$message.error('上传失败了,考虑重试下' + error)
          return
        }
        // 合并切片
        const isUpload = chunkData.some(item => item.uploaded === false)
        if (isUpload) {
          alert('存在失败的切片')
        } else {
          // 执行合并
          try {
            await this.mergeRequest(data)
            resolve()
          } catch (error) {
            reject()
          }
        }
      })
    },
    // 并发处理
    sendRequest(forms, chunkData) {
      var finished = 0
      const total = forms.length
      const that = this
      const retryArr = [] // 数组存储每个文件hash请求的重试次数，做累加 比如[1,0,2],就是第0个文件切片报错1次，第2个报错2次
      return new Promise((resolve, reject) => {
        const handler = () => {
          if (forms.length) {
            // 出栈
            const formInfo = forms.shift()
            const formData = formInfo.formData
            const index = formInfo.index
            axios
              .post(this.url.uploadFileBlock, formData, {
                onUploadProgress: that.createProgresshandler(chunkData[index]),
                cancelToken: new CancelToken(c => this.cancels.push(c)),
                timeout: 0
              })
              .then(res => {
                if (res.success) {
                  // 更改状态
                  chunkData[index].uploaded = true
                  chunkData[index].status = 'success'
                  finished++
                  handler()
                } else {
                  this.$tap.alert('上传异常', 'warning')
                }
              })
              .catch(e => {
                // 若状态为暂停或等待，则禁止重试
                if ([Status.pause, Status.wait].includes(this.status)) return
                if (typeof retryArr[index] !== 'number') {
                  retryArr[index] = 0
                }
                // 更新状态
                chunkData[index].status = 'warning'
                // 累加错误次数
                retryArr[index]++
                // 重试3次
                if (retryArr[index] >= this.chunkRetry) {
                  return reject('重试失败', retryArr)
                }
                this.tempThreads++ // 释放当前占用的通道
                // 将失败的重新加入队列
                forms.push(formInfo)
                handler()
              })
          }
          if (finished >= total) {
            resolve('done')
          }
        }
        // 控制并发
        for (let i = 0; i < this.tempThreads; i++) {
          handler()
        }
      })
    },
    // 通知服务端合并切片
    mergeRequest(data) {
      return new Promise((resolve, reject) => {
        const formData = {
          name: data.name,
          filemd5: data.hash,
          identifier: data.hash,
          filesize: data.filesize,
          totalblockcount: data.chunkList.length,
          sourcetype: this.sourceType,
          sourceid: this.sourceId,
          ...this.uploadArguments
        }
        postAction(this.url.mergeFileBlocks, formData)
          .then(res => {
            if (res.success) {
              data.status = fileStatus.success
              data.uploadProgress = 100
              // 判断是否所有都成功上传
              this.isAllStatus()
              resolve()
            } else {
              data.status = fileStatus.error
              reject()
              this.$tap.alert(res.message, 'warning')
            }
          })
          .catch(err => {
            data.status = fileStatus.error
            reject()
          })
      })
    },
    // 创建文件切片
    createFileChunk(file, size = chunkSize) {
      const fileChunkList = []
      var count = 0
      while (count < file.size) {
        fileChunkList.push({
          file: file.slice(count, count + size)
        })
        count += size
      }
      return fileChunkList
    },
    // 暂停上传
    handlePause() {
      this.status = Status.pause
      if (this.uploadFiles.length) {
        const currentFile = this.uploadFiles[fileIndex]
        currentFile.status = fileStatus.pause
        currentFile.fakeUploadProgress = currentFile.uploadProgress
      }
      // 取消
      while (this.cancels.length > 0) {
        this.cancels.pop()('上传终止')
      }
    },
    // 恢复上传
    handleResume() {
      this.status = Status.uploading
      // console.log('handleResume -> this.uploadFiles[fileIndex]', this.uploadFiles[fileIndex])
      this.uploadFiles[fileIndex].status = fileStatus.resume
      this.handleUpload()
    },
    // 文件上传之前的校验： 校验文件是否已已上传
    verifyUpload(fileName, fileHash) {
      return new Promise(resolve => {
        const formData = {
          qry_identifiers: fileHash,
          qry_sourceid: this.sourceId,
          qry_sourcetype: this.sourceType
        }
        getAction(this.url.queryAlreadyUploadBlocksNum, formData)
          .then(res => {
            resolve(res.result)
          })
          .catch(err => {})
      })
    },
    /** 计算md5，实现断点续传及秒传,注意md5计算也是分片，避免浏览器加载过大*/
    computeMD5(fileObject) {
      return new Promise((re, rj) => {
        let fileReader = new FileReader()
        let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
        let currentBlock = 0
        let chunks = Math.ceil(fileObject.size / chunkSize)
        let spark = new SparkMD5.ArrayBuffer()
        loadNext()
        fileReader.onload = e => {
          spark.append(e.target.result)
          if (currentBlock < chunks) {
            currentBlock++
            loadNext()
          } else {
            var filemd5 = spark.end()
            if (this.uploadFiles[fileIndex]) {
              this.uploadFiles[fileIndex].hashProgress = 100
              this.uploadFiles[fileIndex].hash = filemd5
              this.$set(this.uploadFiles, fileIndex, this.uploadFiles[fileIndex])
            }
            re(this.uploadFiles[fileIndex].hash)
          }
        }
        fileReader.onerror = function() {
          this.error(`文件${fileObject.name}读取出错，请检查该文件`)
          rj()
        }
        function loadNext() {
          let start = currentBlock * chunkSize
          let end = start + chunkSize >= fileObject.size ? fileObject.size : start + chunkSize
          fileReader.readAsArrayBuffer(blobSlice.call(fileObject, start, end))
        }
      })
    },
    // 生成文件 hash（web-worker）
    calculateHash(fileChunkList) {
      return new Promise(resolve => {
        this.worker = new Worker('fileManage/utils/hash.js')
        this.worker.postMessage({ fileChunkList })
        this.worker.onmessage = e => {
          const { percentage, hash } = e.data
          if (this.uploadFiles[fileIndex]) {
            this.uploadFiles[fileIndex].hashProgress = Number(percentage.toFixed(0))
            this.$set(this.uploadFiles, fileIndex, this.uploadFiles[fileIndex])
          }
          if (hash) {
            resolve(hash)
          }
        }
      })
    },
    // 切片上传进度
    createProgresshandler(item) {
      return p => {
        item.progress = parseInt(String((p.loaded / p.total) * 100))
        this.fileProgress()
      }
    },
    // 文件总进度
    fileProgress() {
      const currentFile = this.uploadFiles[fileIndex]
      if (currentFile) {
        const uploadProgress = currentFile.chunkList.map(item => item.size * item.progress).reduce((acc, cur) => acc + cur)
        const currentFileProgress = parseInt((uploadProgress / currentFile.size).toFixed(2))
        // 真假进度条处理--处理进度条后移
        if (!currentFile.fakeUploadProgress) {
          currentFile.uploadProgress = currentFileProgress
          this.$set(this.uploadFiles, fileIndex, currentFile)
        } else if (currentFileProgress > currentFile.fakeUploadProgress) {
          currentFile.uploadProgress = currentFileProgress
          this.$set(this.uploadFiles, fileIndex, currentFile)
        }
      }
    },
    handleClearFiles() {
      if (this.uploadFiles.length > 0) {
        this.$tap
          .confirm('确认清空文件吗？')
          .then(_ => {
            this.clearFiles()
          })
          .catch(_ => {})
      } else {
        this.clearFiles()
      }
    },
    // 清空文件
    clearFiles() {
      fileIndex = 0
      this.handlePause()
      this.worker && this.worker.terminate() // 中断worker
      this.status = Status.wait
      Object.assign(this.$data, this.$options.data()) // 重置data所有数据
    },
    // 判断是否都已完成上传
    isAllStatus() {
      const isAllSuccess = this.uploadFiles.every(item => ['success', 'secondPass', 'error'].includes(item.status))
      if (isAllSuccess) {
        this.status = Status.done
      }
      this.$emit('success')
    },
    /** 文件扩展名 */
    getFileExt(fileName) {
      let arr = fileName.split('.')
      if (arr.length === 0) {
        return ''
      } else {
        return arr[arr.length - 1].toString().toLowerCase()
      }
    }
  }
}
</script>
<style scoped lang="scss">
.simple-upload-container {
  width: 100%;
  border: 1px solid #d2d2d2;
  border-radius: 4px;
  background-color: #fff;
  padding-bottom: 15px;
  padding: 10px;
  .progress-box {
    width: 100%;
  }
  .total-progress {
    margin-bottom: 15px;
    .btns {
      position: relative;
      .select-file-input {
        position: absolute;
        display: inline-block;
        left: 0;
        top: 0;
        border: none;
        opacity: 0;
        width: 96px;
        height: 28px;
      }
    }
  }
  .file-list {
    .list-item {
      padding: 5px 5px;
      display: flex;
      justify-content: center;
      justify-items: center;
      line-height: 25px;
      position: relative;
      &:hover .item-chunk-box {
        display: block;
      }
      div {
        flex: 1;
        margin-top: 6px;
      }
      .item-name {
        flex: 0 0 40%;
        overflow: hidden;
        white-space: nowrap;
        margin-right: 6px;
        .svg-icon {
          font-size: 22px;
          vertical-align: sub;
        }
      }
      .item-status {
        text-align: center;
        text-align: right;
        .el-icon-circle-check {
          color: #67c23a;
        }
        .el-icon-circle-close {
          color: #f00;
        }
      }
      .item-chunk-box {
        display: none;
        transition: all 3s;
        position: absolute;
        top: 0;
        left: 40px;
        z-index: 10;
      }
      .item-progress {
        text-align: right;
        flex: 0 0 40%;
      }
    }
  }
  .upload-tip {
    font-size: 12px;
    color: #606266;
    margin-top: 7px;
  }
  .el-progress {
    width: 50%;
    display: inline-block;
  }
  .el-collapse-item__header {
    height: auto;
  }
}
</style>
